// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using ILLink.Shared;
using Mono.Cecil;

namespace Mono.Linker.Steps
{
    [Flags]
    public enum AllowedAssemblies
    {
        ContainingAssembly = 0x1,
        AnyAssembly = 0x2 | ContainingAssembly,
        AllAssemblies = 0x4 | AnyAssembly
    }

    public abstract class ProcessLinkerXmlBase
    {
        const string FullNameAttributeName = "fullname";
        const string LinkerElementName = "linker";
        const string TypeElementName = "type";
        const string SignatureAttributeName = "signature";
        const string NameAttributeName = "name";
        const string FieldElementName = "field";
        const string MethodElementName = "method";
        const string EventElementName = "event";
        const string PropertyElementName = "property";
        const string AllAssembliesFullName = "*";
        protected const string XmlNamespace = "";

        protected readonly string _xmlDocumentLocation;
        readonly XPathNavigator _document;
        protected readonly (EmbeddedResource Resource, AssemblyDefinition Assembly)? _resource;
        protected readonly LinkContext _context;

        protected ProcessLinkerXmlBase(LinkContext context, Stream documentStream, string xmlDocumentLocation)
        {
            _context = context;
            using (documentStream)
            {
                _document = XDocument.Load(documentStream, LoadOptions.SetLineInfo).CreateNavigator();
            }
            _xmlDocumentLocation = xmlDocumentLocation;
        }

        protected ProcessLinkerXmlBase(LinkContext context, Stream documentStream, EmbeddedResource resource, AssemblyDefinition resourceAssembly, string xmlDocumentLocation)
            : this(context, documentStream, xmlDocumentLocation)
        {
            ArgumentNullException.ThrowIfNull(resource);
            ArgumentNullException.ThrowIfNull(resourceAssembly);

            _resource = (resource, resourceAssembly);
        }

        protected virtual bool ShouldProcessElement(XPathNavigator nav) => FeatureSettings.ShouldProcessElement(nav, _context, _xmlDocumentLocation);

        protected virtual void ProcessXml(bool stripResource, bool ignoreResource)
        {
            if (!AllowedAssemblySelector.HasFlag(AllowedAssemblies.AnyAssembly) && _resource == null)
                throw new InvalidOperationException("The containing assembly must be specified for XML which is restricted to modifying that assembly only.");

            try
            {
                XPathNavigator nav = _document.CreateNavigator();

                // Initial structure check - ignore XML document which don't look like ILLink XML format
                if (!nav.MoveToChild(LinkerElementName, XmlNamespace))
                    return;

                if (_resource != null)
                {
                    if (stripResource)
                        _context.Annotations.AddResourceToRemove(_resource.Value.Assembly, _resource.Value.Resource);
                    if (ignoreResource)
                        return;
                }

                if (!ShouldProcessElement(nav))
                    return;

                ProcessAssemblies(nav);

                // For embedded XML, allow not specifying the assembly explicitly in XML.
                if (_resource != null)
                    ProcessAssembly(_resource.Value.Assembly, nav, warnOnUnresolvedTypes: true);

            }
            catch (Exception ex) when (!(ex is LinkerFatalErrorException))
            {
                throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.ErrorProcessingXmlLocation, _xmlDocumentLocation), ex);
            }
        }

        protected virtual AllowedAssemblies AllowedAssemblySelector { get => _resource != null ? AllowedAssemblies.ContainingAssembly : AllowedAssemblies.AnyAssembly; }

        bool ShouldProcessAllAssemblies(XPathNavigator nav, [NotNullWhen(false)] out AssemblyNameReference? assemblyName)
        {
            assemblyName = null;
            if (GetFullName(nav) == AllAssembliesFullName)
                return true;

            assemblyName = GetAssemblyName(nav);
            return false;
        }

        protected virtual void ProcessAssemblies(XPathNavigator nav)
        {
            foreach (XPathNavigator assemblyNav in nav.SelectChildren("assembly", ""))
            {
                // Errors for invalid assembly names should show up even if this element will be
                // skipped due to feature conditions.
                bool processAllAssemblies = ShouldProcessAllAssemblies(assemblyNav, out AssemblyNameReference? name);
                if (processAllAssemblies && AllowedAssemblySelector != AllowedAssemblies.AllAssemblies)
                {
                    LogWarning(assemblyNav, DiagnosticId.XmlUnsuportedWildcard);
                    continue;
                }

                AssemblyDefinition? assemblyToProcess = null;
                if (!AllowedAssemblySelector.HasFlag(AllowedAssemblies.AnyAssembly))
                {
                    Debug.Assert(!processAllAssemblies);
                    Debug.Assert(_resource != null);
                    if (_resource.Value.Assembly.Name.Name != name!.Name)
                    {
                        LogWarning(assemblyNav, DiagnosticId.AssemblyWithEmbeddedXmlApplyToAnotherAssembly, _resource.Value.Assembly.Name.Name, name.ToString());
                        continue;
                    }
                    assemblyToProcess = _resource.Value.Assembly;
                }

                if (!ShouldProcessElement(assemblyNav))
                    continue;

                if (processAllAssemblies)
                {
                    // We could avoid loading all references in this case: https://github.com/dotnet/linker/issues/1708
                    foreach (AssemblyDefinition assembly in _context.GetReferencedAssemblies())
                        ProcessAssembly(assembly, assemblyNav, warnOnUnresolvedTypes: false);
                }
                else
                {
                    Debug.Assert(!processAllAssemblies);
                    AssemblyDefinition? assembly = assemblyToProcess ?? _context.TryResolve(name!);

                    if (assembly == null)
                    {
                        LogWarning(assemblyNav, DiagnosticId.XmlCouldNotResolveAssembly, name!.Name);
                        continue;
                    }

                    ProcessAssembly(assembly, assemblyNav, warnOnUnresolvedTypes: true);
                }
            }
        }

        protected abstract void ProcessAssembly(AssemblyDefinition assembly, XPathNavigator nav, bool warnOnUnresolvedTypes);

        protected virtual void ProcessTypes(AssemblyDefinition assembly, XPathNavigator nav, bool warnOnUnresolvedTypes)
        {
            foreach (XPathNavigator typeNav in nav.SelectChildren(TypeElementName, XmlNamespace))
            {

                if (!ShouldProcessElement(typeNav))
                    continue;

                string fullname = GetFullName(typeNav);

                if (fullname.Contains('*'))
                {
                    if (ProcessTypePattern(fullname, assembly, typeNav))
                        continue;
                }

                TypeDefinition type = assembly.MainModule.GetType(fullname);

                if (type == null && assembly.MainModule.HasExportedTypes)
                {
                    foreach (var exported in assembly.MainModule.ExportedTypes)
                    {
                        if (fullname == exported.FullName)
                        {
                            var resolvedExternal = ProcessExportedType(exported, assembly, typeNav);
                            if (resolvedExternal != null)
                            {
                                type = resolvedExternal;
                                break;
                            }
                        }
                    }
                }

                if (type == null)
                {
                    if (warnOnUnresolvedTypes)
                        LogWarning(typeNav, DiagnosticId.XmlCouldNotResolveType, fullname);
                    continue;
                }

                ProcessType(type, typeNav);
            }
        }

        protected virtual TypeDefinition? ProcessExportedType(ExportedType exported, AssemblyDefinition assembly, XPathNavigator nav) => _context.TryResolve(exported);

        void MatchType(TypeDefinition type, Regex regex, XPathNavigator nav)
        {
            if (regex.IsMatch(type.FullName))
                ProcessType(type, nav);

            if (!type.HasNestedTypes)
                return;

            foreach (var nt in type.NestedTypes)
                MatchType(nt, regex, nav);
        }

        protected virtual bool ProcessTypePattern(string fullname, AssemblyDefinition assembly, XPathNavigator nav)
        {
            Regex regex = new Regex(fullname.Replace(".", @"\.").Replace("*", "(.*)"));

            foreach (TypeDefinition type in assembly.MainModule.Types)
            {
                MatchType(type, regex, nav);
            }

            if (assembly.MainModule.HasExportedTypes)
            {
                foreach (var exported in assembly.MainModule.ExportedTypes)
                {
                    if (regex.IsMatch(exported.FullName))
                    {
                        var type = ProcessExportedType(exported, assembly, nav);
                        if (type != null)
                        {
                            ProcessType(type, nav);
                        }
                    }
                }
            }

            return true;
        }

        protected abstract void ProcessType(TypeDefinition type, XPathNavigator nav);

        protected void ProcessTypeChildren(TypeDefinition type, XPathNavigator nav, object? customData = null)
        {
            if (nav.HasChildren)
            {
                ProcessSelectedFields(nav, type);
                ProcessSelectedMethods(nav, type, customData);
                ProcessSelectedEvents(nav, type, customData);
                ProcessSelectedProperties(nav, type, customData);
            }
        }

        void ProcessSelectedFields(XPathNavigator nav, TypeDefinition type)
        {
            foreach (XPathNavigator fieldNav in nav.SelectChildren(FieldElementName, XmlNamespace))
            {
                if (!ShouldProcessElement(fieldNav))
                    continue;
                ProcessField(type, fieldNav);
            }
        }

        protected virtual void ProcessField(TypeDefinition type, XPathNavigator nav)
        {
            string signature = GetSignature(nav);
            if (!string.IsNullOrEmpty(signature))
            {
                FieldDefinition? field = GetField(type, signature);
                if (field == null)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindFieldOnType, signature, type.GetDisplayName());
                    return;
                }

                ProcessField(type, field, nav);
            }

            string name = GetName(nav);
            if (!string.IsNullOrEmpty(name))
            {
                bool foundMatch = false;
                if (type.HasFields)
                {
                    foreach (FieldDefinition field in type.Fields)
                    {
                        if (field.Name == name)
                        {
                            foundMatch = true;
                            ProcessField(type, field, nav);
                        }
                    }
                }

                if (!foundMatch)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindFieldOnType, name, type.GetDisplayName());
                }
            }
        }

        protected static FieldDefinition? GetField(TypeDefinition type, string signature)
        {
            if (!type.HasFields)
                return null;

            foreach (FieldDefinition field in type.Fields)
                if (signature == field.FieldType.FullName + " " + field.Name)
                    return field;

            return null;
        }

        protected virtual void ProcessField(TypeDefinition type, FieldDefinition field, XPathNavigator nav) { }

        void ProcessSelectedMethods(XPathNavigator nav, TypeDefinition type, object? customData)
        {
            foreach (XPathNavigator methodNav in nav.SelectChildren(MethodElementName, XmlNamespace))
            {
                if (!ShouldProcessElement(methodNav))
                    continue;
                ProcessMethod(type, methodNav, customData);
            }
        }

        protected virtual void ProcessMethod(TypeDefinition type, XPathNavigator nav, object? customData)
        {
            string signature = GetSignature(nav);
            if (!string.IsNullOrEmpty(signature))
            {
                MethodDefinition? method = GetMethod(type, signature);
                if (method == null)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindMethodOnType, signature, type.GetDisplayName());
                    return;
                }

                ProcessMethod(type, method, nav, customData);
            }

            string name = GetAttribute(nav, NameAttributeName);
            if (!string.IsNullOrEmpty(name))
            {
                bool foundMatch = false;
                if (type.HasMethods)
                {
                    foreach (MethodDefinition method in type.Methods)
                    {
                        if (name == method.Name)
                        {
                            foundMatch = true;
                            ProcessMethod(type, method, nav, customData);
                        }
                    }
                }

                if (!foundMatch)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindMethodOnType, name, type.GetDisplayName());
                }
            }
        }

        protected virtual MethodDefinition? GetMethod(TypeDefinition type, string signature) => null;

        protected virtual void ProcessMethod(TypeDefinition type, MethodDefinition method, XPathNavigator nav, object? customData) { }

        void ProcessSelectedEvents(XPathNavigator nav, TypeDefinition type, object? customData)
        {
            foreach (XPathNavigator eventNav in nav.SelectChildren(EventElementName, XmlNamespace))
            {
                if (!ShouldProcessElement(eventNav))
                    continue;
                ProcessEvent(type, eventNav, customData);
            }
        }

        protected virtual void ProcessEvent(TypeDefinition type, XPathNavigator nav, object? customData)
        {
            string signature = GetSignature(nav);
            if (!string.IsNullOrEmpty(signature))
            {
                EventDefinition? @event = GetEvent(type, signature);
                if (@event == null)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindEventOnType, signature, type.GetDisplayName());
                    return;
                }

                ProcessEvent(type, @event, nav, customData);
            }

            string name = GetAttribute(nav, NameAttributeName);
            if (!string.IsNullOrEmpty(name))
            {
                bool foundMatch = false;
                foreach (EventDefinition @event in type.Events)
                {
                    if (@event.Name == name)
                    {
                        foundMatch = true;
                        ProcessEvent(type, @event, nav, customData);
                    }
                }

                if (!foundMatch)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindEventOnType, name, type.GetDisplayName());
                }
            }
        }

        protected static EventDefinition? GetEvent(TypeDefinition type, string signature)
        {
            if (!type.HasEvents)
                return null;

            foreach (EventDefinition @event in type.Events)
                if (signature == @event.EventType.FullName + " " + @event.Name)
                    return @event;

            return null;
        }

        protected virtual void ProcessEvent(TypeDefinition type, EventDefinition @event, XPathNavigator nav, object? customData) { }

        void ProcessSelectedProperties(XPathNavigator nav, TypeDefinition type, object? customData)
        {
            foreach (XPathNavigator propertyNav in nav.SelectChildren(PropertyElementName, XmlNamespace))
            {
                if (!ShouldProcessElement(propertyNav))
                    continue;
                ProcessProperty(type, propertyNav, customData);
            }
        }

        protected virtual void ProcessProperty(TypeDefinition type, XPathNavigator nav, object? customData)
        {
            string signature = GetSignature(nav);
            if (!string.IsNullOrEmpty(signature))
            {
                PropertyDefinition? property = GetProperty(type, signature);
                if (property == null)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindPropertyOnType, signature, type.GetDisplayName());
                    return;
                }

                ProcessProperty(type, property, nav, customData, true);
            }

            string name = GetAttribute(nav, NameAttributeName);
            if (!string.IsNullOrEmpty(name))
            {
                bool foundMatch = false;
                foreach (PropertyDefinition property in type.Properties)
                {
                    if (property.Name == name)
                    {
                        foundMatch = true;
                        ProcessProperty(type, property, nav, customData, false);
                    }
                }

                if (!foundMatch)
                {
                    LogWarning(nav, DiagnosticId.XmlCouldNotFindPropertyOnType, name, type.GetDisplayName());
                }
            }
        }

        protected static PropertyDefinition? GetProperty(TypeDefinition type, string signature)
        {
            if (!type.HasProperties)
                return null;

            foreach (PropertyDefinition property in type.Properties)
                if (signature == property.PropertyType.FullName + " " + property.Name)
                    return property;

            return null;
        }

        protected virtual void ProcessProperty(TypeDefinition type, PropertyDefinition property, XPathNavigator nav, object? customData, bool fromSignature) { }

        protected virtual AssemblyNameReference GetAssemblyName(XPathNavigator nav)
        {
            return AssemblyNameReference.Parse(GetFullName(nav));
        }

        protected static string GetFullName(XPathNavigator nav)
        {
            return GetAttribute(nav, FullNameAttributeName);
        }

        protected static string GetName(XPathNavigator nav)
        {
            return GetAttribute(nav, NameAttributeName);
        }

        protected static string GetSignature(XPathNavigator nav)
        {
            return GetAttribute(nav, SignatureAttributeName);
        }

        protected static string GetAttribute(XPathNavigator nav, string attribute)
        {
            return nav.GetAttribute(attribute, XmlNamespace);
        }

        protected MessageOrigin GetMessageOriginForPosition(XPathNavigator position)
        {
            return (position is IXmlLineInfo lineInfo)
                    ? new MessageOrigin(_xmlDocumentLocation, lineInfo.LineNumber, lineInfo.LinePosition, _resource?.Assembly)
                    : new MessageOrigin(_xmlDocumentLocation, 0, 0, _resource?.Assembly);
        }
        protected void LogWarning(string message, int warningCode, XPathNavigator position)
        {
            _context.LogWarning(message, warningCode, GetMessageOriginForPosition(position));
        }

        protected void LogWarning(XPathNavigator position, DiagnosticId id, params string[] args)
        {
            _context.LogWarning(GetMessageOriginForPosition(position), id, args);
        }

        public override string ToString() => GetType().Name + ": " + _xmlDocumentLocation;

        public bool TryConvertValue(string value, TypeReference target, out object? result)
        {
            switch (target.MetadataType)
            {
                case MetadataType.Boolean:
                    if (bool.TryParse(value, out bool bvalue))
                    {
                        result = bvalue ? 1 : 0;
                        return true;
                    }

                    goto case MetadataType.Int32;

                case MetadataType.Byte:
                    if (!byte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte byteresult))
                        break;

                    result = (int)byteresult;
                    return true;

                case MetadataType.SByte:
                    if (!sbyte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out sbyte sbyteresult))
                        break;

                    result = (int)sbyteresult;
                    return true;

                case MetadataType.Int16:
                    if (!short.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out short shortresult))
                        break;

                    result = (int)shortresult;
                    return true;

                case MetadataType.UInt16:
                    if (!ushort.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ushort ushortresult))
                        break;

                    result = (int)ushortresult;
                    return true;

                case MetadataType.Int32:
                    if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int iresult))
                        break;

                    result = iresult;
                    return true;

                case MetadataType.UInt32:
                    if (!uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint uresult))
                        break;

                    result = (int)uresult;
                    return true;

                case MetadataType.Double:
                    if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double dresult))
                        break;

                    result = dresult;
                    return true;

                case MetadataType.Single:
                    if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float fresult))
                        break;

                    result = fresult;
                    return true;

                case MetadataType.Int64:
                    if (!long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long lresult))
                        break;

                    result = lresult;
                    return true;

                case MetadataType.UInt64:
                    if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong ulresult))
                        break;

                    result = (long)ulresult;
                    return true;

                case MetadataType.Char:
                    if (!char.TryParse(value, out char chresult))
                        break;

                    result = (int)chresult;
                    return true;

                case MetadataType.String:
                    if (value is string || value == null)
                    {
                        result = value;
                        return true;
                    }

                    break;

                case MetadataType.ValueType:
                    if (value is string &&
                        _context.TryResolve(target) is TypeDefinition typeDefinition &&
                        typeDefinition.IsEnum)
                    {
                        var enumField = typeDefinition.Fields.Where(f => f.IsStatic && f.Name == value).FirstOrDefault();
                        if (enumField != null)
                        {
                            result = Convert.ToInt32(enumField.Constant);
                            return true;
                        }
                    }

                    break;
            }

            result = null;
            return false;
        }
    }
}
